﻿using System;
using System.Collections.Generic;
using System.Diagnostics;
using System.IO;
using System.Linq;
using System.Text;
using System.Web;
using Birkelund.QueryStrings.Internals;

namespace Birkelund.QueryStrings
{
    /// <summary>
    /// Class for creating full urls with strongly typed querystring tokens
    /// </summary>
    public class UriWriter : IUriWriter
    {
        #region Members
        [DebuggerBrowsable(DebuggerBrowsableState.Never)]
        private readonly Uri _baseUri = null;

        [DebuggerBrowsable(DebuggerBrowsableState.Never)]
        private string _fileName = null;

        [DebuggerBrowsable(DebuggerBrowsableState.Never)]
        private readonly IList<string> _directoryFragments;

        [DebuggerBrowsable(DebuggerBrowsableState.Never)]
        private IList<TokenKeyValue> _innerTokens = null;

        /// <summary>
        /// Set this property to add a checksum token to the querystring. If required the token reader will validate that all query tokens are not modified by end user.
        /// </summary>
        public bool ApplyCheckSumControl { get; set; }

        /// <summary>
        /// Determines whether the generated url should use SSL or not. <c>null</c> will leave the choice up to initial URL.
        /// </summary>
        public bool? UseHttps { get; set; }

        /// <summary>
        /// Get or Set whether the tokens should be sorted alphabetically. If false the tokens will be written in the order they are added.
        /// </summary>
        public bool SortTokens { get; set; }

        /// <summary>
        /// The fragment (eg. bookmark to navigate to)
        /// </summary>
        /// <value>The fragment.</value>
        public string Fragment { get; set; }

        /// <summary>
        /// Calculates the length of the QueryString AFTER the ?-mark
        /// </summary>
        /// <returns>Returns the QueryString length</returns>
        public int QueryStringLength
        {
            get
            {
                return BuildQueryString(_innerTokens).Length;
            }
        }
        #endregion
        /// <summary>
        /// Constructor to instantiate this class. 
        /// </summary>
        /// <param name="uri">The url to base the UriWriter on</param>
        private UriWriter(Uri uri)
        {
            ArgumentValidation.ThrowIfNull(uri, "uri");
            _baseUri = uri;
            _innerTokens = HttpUtility.ParseQueryString(_baseUri.Query)
                .ToTokenKeyValues()
                .ToList();

            SortTokens = true;
            ApplyCheckSumControl = false;

            if (!string.IsNullOrEmpty(uri.Fragment))
            {
                //See Request for Comments (rfc5147)
                Fragment = uri.Fragment.Substring(1); //first char is #
            }

            _directoryFragments = ParsePathFragments(uri);
            _fileName = ParseFileName(uri);
        }

        #region Static instantiate methods
        /// <summary>
        /// Creates a new instance of this class based on the current uri.
        /// </summary>
        /// <returns>A new class instance</returns>
        public static IUriWriter CreateFromCurrentUri()
        {
            return CreateFromUri(Common.CurrentUrl);
        }

        /// <summary>
        /// Creates a new instance of this class based on the specified uri.
        /// </summary>
        /// <returns>A new class instance</returns>
        public static IUriWriter CreateFromUri(Uri uri)
        {
            return new UriWriter(uri);
        }

        /// <summary>
        /// Creates a new instance of this class based on the specified uri.
        /// </summary>
        /// <returns>A new class instance</returns>
        public static IUriWriter CreateFromUri(string uri)
        {
            return CreateFromUri(new Uri(uri));
        }

        /// <summary>
        /// Creates a new instance of this class based on the specified uri. All querystring parameters will be removed
        /// </summary>
        /// <param name="uri">The uri</param>
        /// <returns>A new class instance</returns>
        public static IUriWriter CreateEmptyFromUri(Uri uri)
        {
            UriBuilder builder = new UriBuilder(uri);
            builder.Query = null;

            return CreateFromUri(builder.Uri);
        }

        /// <summary>
        /// Creates a new instance of this class based on the specified uri. All querystring parameters will be removed
        /// </summary>
        /// <param name="uri">The uri</param>
        /// <returns>A new class instance</returns>
        public static IUriWriter CreateEmptyFromUri(string uri)
        {
            return CreateEmptyFromUri(new Uri(uri));
        }

        /// <summary>
        /// Creates a new instance of this class based on the current uri. All querystring parameters will be removed
        /// </summary>
        /// <returns>A new class instance</returns>
        public static IUriWriter CreateEmptyFromCurrentUri()
        {
            return CreateEmptyFromUri(Common.CurrentUrl);
        }
        #endregion

        #region Public methods
        /// <summary>
        /// Counts how many tokens are currently in the querystring token collection.
        /// </summary>
        /// <returns></returns>
        public int CountTokens()
        {
            return _innerTokens.Count;
        }

        /// <summary>
        /// Set/add a token value. If the token key already exists the entire token will be overwritten.
        /// </summary>
        /// <typeparam name="T">Any valid type - dependent on the current token provider</typeparam>
        /// <param name="token">Token</param>
        /// <param name="value">The value of the token</param>
        public IUriWriter SetToken<T>(IQueryStringToken<T> token, T value)
        {
            //remove token if it already exists
            RemoveTokens(token);

            string renderedValue = token.RenderTokenValue(value);
            if (token is IRenderIfNullOrEmptyValue) //check if the token shoud be rendered at all
            {
                if (string.IsNullOrEmpty(renderedValue) && !((IRenderIfNullOrEmptyValue)token).RenderIfEmptyValue)
                {
                    return this;
                }
            }

            _innerTokens.Add(new TokenKeyValue(token.KeyName, renderedValue));
            return this;
        }

        public IUriWriter DirectoryAdd(string directoryName)
        {
            ArgumentValidation.ThrowIfNull(directoryName, "directoryName");

            if (directoryName == "~")
            {
                _directoryFragments.Clear();
                return this;
            }
            else
            {
                foreach (char c in directoryName)
                {
                    if (Path.GetInvalidPathChars().Contains(c))
                    {
                        throw new ArgumentException("Invalid chars in directory name");
                    }
                }
                _directoryFragments.Add(directoryName);
                return this;
            }
        }

        public IUriWriter DirectoryBack(int numberOfDirectoriesToGoBack)
        {
            if (numberOfDirectoriesToGoBack < 0)
            {
                throw new ArgumentException("Number must be positive ");
            }

            for (int i = 0; i < numberOfDirectoriesToGoBack; i++)
            {
                if (_directoryFragments.Count > 0)
                {
                    _directoryFragments.RemoveAt(_directoryFragments.Count - 1);
                }
                else
                {
                    break;
                }

            }
            return this;
        }

        public IUriWriter SetFileName(string fileName)
        {
            if (string.IsNullOrEmpty(fileName))
            {
                _fileName = null;
                return this;
            }

            ArgumentValidation.ThrowIfInvalidFileName(fileName);

            _fileName = fileName;
            return this;
        }

        /// <summary>
        /// Removes one or more elements identified by keynames. (Not case sensitive)
        /// </summary>
        /// <param name="keys">Key(s) to remove. If a key doesn't exists nothing will happend.</param>
        public IUriWriter RemoveTokens(params ITokenKey[] keys)
        {
            _innerTokens = _innerTokens.GetCollectionWithRemovedKeys(keys);
            return this;
        }

        /// <summary>
        /// Removes all tokens from the list
        /// </summary>
        public IUriWriter RemoveTokenAll()
        {
            _innerTokens.Clear();
            return this;
        }

        /// <summary>
        /// Removes all tokens which are not included in the token list
        /// </summary>
        /// <param name="keys">Keys which should NOT be removed</param>
        public IUriWriter RemoveTokensAllBut(params ITokenKey[] keys)
        {
            List<TokenKeyValue> tmp = new List<TokenKeyValue>();

            foreach (ITokenKey key in keys)
            {
                foreach (TokenKeyValue item in _innerTokens)
                {
                    if (item.Key == key.KeyName)
                    {
                        tmp.Add(new TokenKeyValue(item.Key, item.Value));
                    }
                }
            }
            _innerTokens = tmp;

            return this;
        }

        /// <summary>
        /// Generate the query as a string
        /// </summary>
        /// <returns></returns>
        public string GenerateQueryAsString()
        {
            return GetUriBuilder().ToString();
        }

        /// <summary>
        /// Generate the query as a <seealso cref="Uri"/>
        /// </summary>
        /// <returns></returns>
        public Uri GenerateQueryAsUri()
        {
            return GetUriBuilder().Uri;
        }

        /// <summary>
        /// Gets the UriBuilder which is used for full query generation. 
        /// Use this method to manipulate further with the query.
        /// </summary>
        /// <returns>An instance of UriBuilder</returns>
        public UriBuilder GetUriBuilder()
        {
            UriBuilder builder = new UriBuilder(_baseUri);

            string path = string.Empty;
            foreach (string directory in _directoryFragments)
            {
                path += directory + "/";
            }
            if (!string.IsNullOrEmpty(_fileName))
            {
                path += _fileName;
            }
            builder.Path = path;

            builder.Query = BuildQueryString(_innerTokens);
            builder.Fragment = Fragment;

            if (UseHttps.HasValue)
            {
                builder.Scheme = UseHttps.Value ? Uri.UriSchemeHttps : Uri.UriSchemeHttp;
            }
            if (_baseUri.IsDefaultPort)
            {
                builder.Port = -1;
            }

            return builder;
        }

        ///<summary>
        ///This method returns the same result as GenerateQueryAsString()
        ///</summary>
        ///<returns></returns>
        public override string ToString()
        {
            return GenerateQueryAsString();
        }

        ///<summary>
        ///Navigate to url generated by the GenerateQueryAsString() method
        ///</summary>
        ///<exception cref="NullReferenceException">If outside httpcontext></exception>
        public void Navigate()
        {
            string query = GenerateQueryAsString();
            if (query.Length == 0)
            {
                throw new ArgumentException("No url specified");
            }
            else
            {
                HttpContext.Current.Response.Redirect(query);
            }
        }
        #endregion

        #region Private methods
        /// <summary>
        /// Builds the query string.
        /// </summary>
        /// <returns></returns>
        private string BuildQueryString(ICollection<TokenKeyValue> tokens)
        {
            if (tokens.Count == 0)
            {
                return string.Empty;
            }
            else
            {
                const char TOKEN_SEPERATOR = '&';
                const int ESTIMATED_LENGTH_PER_TOKEN = 10; // this is a wild guess on the length of each key and value appended
                StringBuilder sb = new StringBuilder(tokens.Count * ESTIMATED_LENGTH_PER_TOKEN);

                //Remove this key and recalculate it if required. No exception if key doesn't exist.
                tokens = tokens.GetCollectionWithRemovedKeys(CheckSumKeyToken.Instance);

                if (ApplyCheckSumControl)
                {
                    ArgumentValidation.ThrowIfNull(QueryStringConfiguration.Instance.ChecksumCalculator, "ChecksumCalculator is not set. Apply on QueryStringConfiguration");

                    string checkSumValue = QueryStringConfiguration.Instance.ChecksumCalculator.CalculateCheckSum(tokens.GetTokensWhichAreNotChecksumToken());
                    tokens.Add(new TokenKeyValue(CheckSumKeyToken.Instance.KeyName, checkSumValue));
                }
                if (SortTokens)
                {
                    IComparer<string> com = new AlphabeticWithCheckSumLastComparer();
                    tokens = tokens.OrderBy(x => x.Key, com).ToList();
                }

                foreach (TokenKeyValue token in tokens)
                {
                    sb.AppendFormat("{0}={1}", token.Key, token.Value);
                    sb.Append(TOKEN_SEPERATOR);
                }

                int removeIndex = sb.Length - TOKEN_SEPERATOR.ToString().Length;
                string query = sb.ToString().Remove(removeIndex);

                if (QueryStringConfiguration.Instance.Encryptor == null)
                {
                    return query;
                }
                else
                {
                    return FullEncryptionKeyToken.Instance.KeyName + "=" + QueryStringConfiguration.Instance.Encryptor.Encrypt(query);
                }
            }
        }

        private IList<string> ParsePathFragments(Uri uri)
        {
            string absUri = Path.GetDirectoryName(uri.AbsolutePath);
            if (absUri == null || absUri == @"/")
            {
                return new List<string>();
            }

            string[] directories = absUri.Split(new[]{'/', '\\'}, StringSplitOptions.RemoveEmptyEntries);

            return directories.ToList();
        }

        private string ParseFileName(Uri uri)
        {
            string filename = Path.GetFileName(uri.AbsolutePath);

            return filename;
        }
        #endregion
    }
}